#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include <napi.h>
#include <Windows.h>
#include <Winspool.h>
#include <wincodec.h>
#include <heapapi.h>
#include <unknwn.h>
#include <tchar.h>
#include <iostream>
#include <winuser.h>
#include <wingdi.h>
#include <comdef.h>
#include <gdiplus.h>
#include <gdiplusheaders.h>
#include <errhandlingapi.h>
#include <winbase.h>
#include <String>

using namespace Napi;
using namespace Gdiplus;
#pragma comment(lib, "Gdiplus.lib")

template<class T>
void SafeRelease(T **ppT){
    if(*ppT){
        (*ppT)->Release();
        *ppT=NULL;
    }
}

//solve Chinese character code
// convert UTF-8 to Unicode first, and then convert to GBK
char* utf8ToGBK(char* strUTF) {
  int size;
  size = MultiByteToWideChar(CP_UTF8,0,strUTF,-1,NULL,0); 
  wchar_t* strUnicode = new wchar_t[size];
  MultiByteToWideChar (CP_UTF8,0,strUTF,-1,strUnicode,size);

  size= WideCharToMultiByte(CP_ACP,0,strUnicode,-1,NULL,0,NULL,NULL);
  char *strGBK = new char[size];
  WideCharToMultiByte (CP_ACP,0,strUnicode,-1,strGBK,size,NULL,NULL); 
  delete []strUnicode;
  return strGBK;
}

//-----------------------------------------------------------------------------
static void report_error(LPTSTR message,  DWORD error_code) {
//-----------------------------------------------------------------------------
    LPTSTR lpMsgBuf = 0;
    DWORD message_size = 0;

    message_size = FormatMessage( 
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        error_code,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
        lpMsgBuf,
        0,
        NULL 
    );

    printf("%ws 0x%04X %ws\n",  message, error_code, lpMsgBuf);

    // Free the buffer.
    LocalFree(lpMsgBuf);

} // static void report_error(LPTSTR message,  DWORD error_code)

std::string _descPaperSize(short paperSize){
  switch (paperSize) {
    case DMPAPER_LETTER:
      return std::string("Latter");
    case DMPAPER_LEGAL:
      return std::string("Legal");
    case DMPAPER_9X11:
      return std::string("9X11(in)");
    case DMPAPER_10X11:
      return std::string("10X11(in)");
    case DMPAPER_10X14:
      return std::string("10X14(in)");
    case DMPAPER_15X11:
      return std::string("15X11(in)");
    case DMPAPER_11X17:
      return std::string("11X17(in)");
    case DMPAPER_12X11:
      return std::string("12X11(in)");
    case DMPAPER_A2:
      return std::string("A2");
    case DMPAPER_A3:
      return std::string("A3");
    case DMPAPER_A3_ROTATED:
      return std::string("A3 Rotated");
    case DMPAPER_A4:
      return std::string("A4");
    case DMPAPER_A5:
      return std::string("A5");
    // values from win10
    case 119:
      return std::string("4X6(in)");
    case 120:
      return std::string("4X8(in)");
    case 121:
      return std::string("5X7(in)");
    case 127:
      return std::string("4X7.1(in)");
    
    default:
      printf("unknown paper size %d\n", paperSize);
      return "other";
  }
}

// 
BOOL _getPrinters(LPTSTR pPrinterName, void** ppInfo, LPDWORD pDwReturned) {
  DWORD dwNeeded = 0;
  DWORD dwReturned = 0;
  DWORD level = 2L;
  LPBYTE pInfo = NULL;

  if (pPrinterName != NULL) {
    level = 1L;
  }
  BOOL fnReturn = EnumPrinters(
      PRINTER_ENUM_LOCAL | PRINTER_ENUM_NAME,
      pPrinterName,
      level,
      (LPBYTE)NULL,
      0L,
      &dwNeeded,
      &dwReturned
  );

  if (dwNeeded > 0) {
    pInfo = (LPBYTE)HeapAlloc(GetProcessHeap(), 0L, dwNeeded);
  }

  if (NULL != pInfo) {
    dwReturned = 0;
    fnReturn = EnumPrinters(
      PRINTER_ENUM_LOCAL | PRINTER_ENUM_NAME,
      pPrinterName,
      level,
      (LPBYTE)pInfo,
      dwNeeded,
      &dwNeeded,
      &dwReturned
    );

    if (fnReturn) {
      *ppInfo = pInfo;
      *pDwReturned = dwReturned;
      return TRUE;
    }
  }

  return FALSE;
}

Napi::Object _getGlobalDefaultOptions(Napi::Env env, LPDEVMODE pDevMode) {
  Napi::Object options = Napi::Object::New(env);

  options.Set("friendlyName", (char16_t *)(pDevMode->dmDeviceName));
  // paperSize
  if (pDevMode->dmFields & DM_PAPERSIZE) {
    options.Set("pageSize", _descPaperSize(pDevMode->dmPaperSize));
  }
  // paperLength & paperWidth
  if (pDevMode->dmFields & DM_PAPERLENGTH) {
    options.Set("paperLength", pDevMode->dmPaperLength);
  }
  if (pDevMode->dmFields & DM_PAPERWIDTH) {
    options.Set("paperWidth", pDevMode->dmPaperWidth);
  }
  // Orientation
  if (pDevMode->dmFields & DM_ORIENTATION) {
    options.Set("orientation", pDevMode->dmOrientation);
  }

  return options;
}

Napi::Value _getUserDefaultOptions(Napi::Env env, std::string printerName) {
  Napi::Object options = Napi::Object::New(env);

  // Open printer
#ifdef UNICODE
  TCHAR pPrinterName[256];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    printerName.c_str(), printerName.size() + 1,
    pPrinterName, ARRAYSIZE(pPrinterName)
  );
#else
  LPTSTR pPrinterName = (LPTSTR)printerName.c_str();
#endif
  HANDLE hPrinter;
  BOOL fnReturn = OpenPrinter(pPrinterName, &hPrinter, NULL);
  if (!fnReturn){
    Napi::TypeError::New(env, "_getUserDefaultOptions Open Printer failed")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  // get printer details
  DWORD dwNeeded = 0;
  PRINTER_INFO_9* pInfo = NULL;
  fnReturn = GetPrinter(hPrinter, 9L, (LPBYTE)NULL, 0L, &dwNeeded);
  
  if (dwNeeded > 0) {
    pInfo = (PRINTER_INFO_9*)HeapAlloc(GetProcessHeap(), 0L, dwNeeded);

    fnReturn = GetPrinter(hPrinter, 9L, (LPBYTE)pInfo, dwNeeded, &dwNeeded);
    if (fnReturn && pInfo->pDevMode) {
      LPDEVMODE pDevMode = pInfo->pDevMode;
      options.Set("friendlyName", (char16_t*)pDevMode->dmDeviceName);
      // paperSize
      if (pDevMode->dmFields & DM_PAPERSIZE) {
        options.Set("pageSize", _descPaperSize(pDevMode->dmPaperSize));
      }
      // paperLength & paperWidth
      if (pDevMode->dmFields & DM_PAPERLENGTH) {
        options.Set("paperLength", pDevMode->dmPaperLength);
      }
      if (pDevMode->dmFields & DM_PAPERWIDTH) {
        options.Set("paperWidth", pDevMode->dmPaperWidth);
      }
      // Orientation
      if (pDevMode->dmFields & DM_ORIENTATION) {
        options.Set("orientation", pDevMode->dmOrientation);
      }
      if (pDevMode->dmFields & DM_SCALE) {
        options.Set("scale", pDevMode->dmScale);
      }
      // printQuality
      if (pDevMode->dmFields & DM_PRINTQUALITY) {
        options.Set("dpi", pDevMode->dmPrintQuality);
      }
    }

    // free memory
    HeapFree(GetProcessHeap(), 0L, pInfo);
  }

  // close printer
  ClosePrinter(hPrinter);

  return options;
}

Napi::Array _getPrinterJobs(Napi::Env env, std::string printerName) {
  Napi::Array jobs = Napi::Array::New(env);

  // Open printer
#ifdef UNICODE
  TCHAR pPrinterName[256];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    printerName.c_str(), printerName.size() + 1,
    pPrinterName, ARRAYSIZE(pPrinterName)
  );
#else
  LPTSTR pPrinterName = (LPTSTR)printerName.c_str();
#endif
  HANDLE hPrinter;
  BOOL fnReturn = OpenPrinter(pPrinterName, &hPrinter, NULL);
  if (!fnReturn){
    Napi::TypeError::New(env, "Open Printer failed")
        .ThrowAsJavaScriptException();
    return jobs;
  }

  // get print jobs
  DWORD dwNeeded = 0;
  DWORD dwReturned = 0;
  fnReturn = EnumJobs(hPrinter, 
        0L            /* FirstJob */, 
        1000L         /* NoJobs */, 
        1L            /* Level */, 
        (LPBYTE)NULL  /* pJob */, 
        0L            /* cbBuf */, 
        &dwNeeded,
        &dwReturned);
  if (dwNeeded > 0) {
    JOB_INFO_1* pJob = (JOB_INFO_1*)HeapAlloc(GetProcessHeap(), 0L, dwNeeded);
    if (!pJob) {
      Napi::TypeError::New(env, "Allocate memory failed")
          .ThrowAsJavaScriptException();
      ClosePrinter(hPrinter);
      return jobs;
    }

    fnReturn = EnumJobs(hPrinter, 
          0L          /* FirstJob */, 
          1000L       /* NoJobs */, 
          1L          /* Level */, 
          (LPBYTE)pJob, 
          dwNeeded    /* cbBuf */, 
          &dwNeeded,
          &dwReturned);
    if (!fnReturn) {
      Napi::TypeError::New(env, "Get Printer Jobs failed")
          .ThrowAsJavaScriptException();
      ClosePrinter(hPrinter);
      return jobs;
    }

    for (unsigned int i=0; i<dwReturned; i++) {
      JOB_INFO_1* pThisInfo = pJob + i;
      Napi::Object job = Napi::Object::New(env);
      job.Set("jobId", Napi::Number::New(env, pThisInfo->JobId));
      job.Set("document", Napi::String::New(env, (char16_t *)pThisInfo->pDocument));
      if (pThisInfo->pStatus) {
        job.Set("status", Napi::String::New(env, (char16_t *)pThisInfo->pStatus));
      }
      job.Set("state", Napi::Number::New(env, pThisInfo->Status));

      jobs[i] = job;
    }

    // clean memory
    HeapFree(GetProcessHeap(), 0L, pJob);
  }

  ClosePrinter(hPrinter);

  return jobs;
}

BOOL changeConfig(LPTSTR pPrinterName, Napi::Object options) {
  // Open printer
  HANDLE hPrinter;
  PRINTER_DEFAULTS pd;
  PRINTER_INFO_9 *pInfo = NULL;
  ZeroMemory(&pd, sizeof(pd));
  pd.DesiredAccess = PRINTER_ACCESS_USE;
  BOOL fnReturn = OpenPrinter(pPrinterName, &hPrinter, &pd);
  if (!fnReturn){
    report_error(L"changeConfig Open printer failed.", GetLastError());
    return FALSE;
  }

  // origin config
  DEVMODE *pDevMode = NULL;
  DWORD dwNeeded = 0;
  fnReturn = GetPrinter(hPrinter, 9L, NULL, 0L, &dwNeeded);
  if (dwNeeded > 0) {
    pInfo = (PRINTER_INFO_9*)GlobalAlloc(GPTR, dwNeeded);
    if (pInfo == NULL) {
      ClosePrinter(hPrinter);
      report_error(L"Allocate memory for pInfo failed", GetLastError());
      return FALSE;
    }

    GetPrinter(hPrinter, 9L, (LPBYTE)pInfo, dwNeeded, &dwNeeded);
    // If GetPrinter didn't fill in the DEVMODE, try to get it by calling DocumentProperties
    if (pInfo->pDevMode == NULL) {
      dwNeeded = DocumentProperties(NULL, hPrinter, pPrinterName, NULL, NULL, 0);
      if (dwNeeded <= 0) {
        report_error(L"Try to get printer info failed.", GetLastError());
        GlobalFree(pInfo);
        ClosePrinter(hPrinter);
        return FALSE;
      }

      pDevMode = (DEVMODE*)GlobalAlloc(GPTR, dwNeeded);
      fnReturn = DocumentProperties(NULL, hPrinter, pPrinterName, pDevMode, NULL, DM_OUT_BUFFER);
      if (!fnReturn || pDevMode == NULL) {
        report_error(L"Get printer info failed.", GetLastError());
        GlobalFree(pDevMode);
        GlobalFree(pInfo);
        ClosePrinter(hPrinter);
        return FALSE;
      }
      pInfo->pDevMode = pDevMode;
    }

    // Do not attemp to set security descriptor
    // pInfo->pSecurityDescriptor = NULL;

    // Make sure the driver-dependort part of devmode is updated
    DocumentProperties(NULL, hPrinter, pPrinterName, pInfo->pDevMode, pInfo->pDevMode, DM_IN_BUFFER | DM_OUT_BUFFER);

    // set printer configuration
    // pInfo->pDevMode->dmFields = 0;
    // DPI
    if (options.Has("dpi")) {
      UINT dpi = options.Get("dpi").As<Napi::Number>().Uint32Value();
      pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_PRINTQUALITY;
      pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_YRESOLUTION;
      pInfo->pDevMode->dmPrintQuality = dpi;
      pInfo->pDevMode->dmYResolution = dpi;
    }
    // Orientation
    if (options.Has("orientation")) {
      UINT orientation = 0;
      if (options.Get("orientation").As<Napi::String>().Utf8Value() == "Portrait") {
        orientation = 1;
      } else if (options.Get("orientation").As<Napi::String>().Utf8Value() == "Landscape") {
        orientation = 2;
      }
      pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_ORIENTATION;
      pInfo->pDevMode->dmOrientation = orientation;
    }
    // keep origin paper size
    pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_PAPERSIZE;
    // keep media type
    pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_MEDIATYPE;
    // copies
    UINT copies = 1;
    pInfo->pDevMode->dmFields = pInfo->pDevMode->dmFields | DM_COPIES;
    if (options.Has("copies")) {
      copies = options.Get("copies").As<Napi::Number>().Uint32Value();
    }
    pInfo->pDevMode->dmCopies = copies;

    fnReturn = SetPrinter(hPrinter, 9L, (LPBYTE)pInfo, 0);
    if (!fnReturn) {
      report_error(L"SetPrinter failed.", GetLastError());
    }
    
  }

  // close
  if (pInfo) {
    GlobalFree(pInfo);
  }
  if (hPrinter) {
    ClosePrinter(hPrinter);
  }
  if (pDevMode) {
    GlobalFree(pDevMode);
  }
  
  return fnReturn;
}

DWORD _printImg(std::string filename, std::string printerName, Napi::Object options) {
#ifdef UNICODE
  TCHAR pPrinterName[256];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    printerName.c_str(), printerName.size() + 1,
    pPrinterName, ARRAYSIZE(pPrinterName)
  );
#else
  LPTSTR pPrinterName = (LPTSTR)printerName.c_str();
#endif
  // change config
  changeConfig(pPrinterName, options);

  Status status;
  // init Gdiplus
  ULONG_PTR gdiplusToken;
  GdiplusStartupInput startupInput;
  status = GdiplusStartup(&gdiplusToken, &startupInput, NULL);
  if (status != Ok) {
    std::cout << "GdiplusStartup failed: " << status << std::endl;
    return NULL;
  }

  HDC hPrtDC = CreateDC(NULL, (LPCWSTR)pPrinterName, NULL, NULL);
  if (hPrtDC <= 0) {
    std::cout << "CreateDC Open printer failed: " << printerName << std::endl;
    return 0;
  }
  HDC hMemDC = CreateCompatibleDC(hPrtDC);

  // load image
#ifdef UNICODE
  TCHAR pFilename[MAX_PATH];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    filename.c_str(), filename.size() + 1,
    pFilename, ARRAYSIZE(pFilename)
  );
#else
  LPTSTR pFilename = (LPTSTR)filename.c_str();
#endif
  Bitmap mBitmap(pFilename);
  HBITMAP hBitmap;
  status = mBitmap.GetHBITMAP(0x000000, &hBitmap);
  if (status != Ok) {
    std::cout << "Get HBITMAP failed: Status: " << status << " #filename: " << filename << std::endl;
    return 0;
  }
  SelectObject(hMemDC, hBitmap);

  // start printing
  DOCINFO docInfo;
  ZeroMemory(&docInfo, sizeof(DOCINFO));
  docInfo.cbSize = sizeof(DOCINFO);
  docInfo.lpszDocName = pFilename;
  DWORD jobId = StartDoc(hPrtDC, &docInfo);
  StartPage(hPrtDC);

  // determin width and height according options.fit and orientation
  int paperWidth = GetDeviceCaps(hPrtDC, HORZRES);
  int paperHeight = GetDeviceCaps(hPrtDC, VERTRES);
  int mWidth = mBitmap.GetWidth();
  int mHeight = mBitmap.GetHeight();
  int mX = 0, mY = 0, pX = 0, pY = 0;
  int w = paperWidth, h = paperHeight;

  // default: stretch
  if (options.Has("fit") && options.Get("fit").As<Napi::String>().Utf8Value() == "cover") {
    if ((float)mWidth/mHeight > (float)paperWidth/paperHeight) {
      h = paperHeight;
      w = h * mWidth / mHeight;
      pX = (paperWidth - w) / 2;
    } else {
      w = paperWidth;
      h = w * mHeight / mWidth;
      pY = (paperHeight - h) / 2;
    }
    
  } else if (options.Has("fit") && options.Get("fit").As<Napi::String>().Utf8Value() == "contain") {
    if ((float)mWidth/mHeight > (float)paperWidth/paperHeight) {
      w = paperWidth;
      h = w * mHeight / mWidth;
      pY = (paperHeight - h) / 2;
    } else {
      h = paperHeight;
      w = h * mWidth / mHeight;
      pX = (paperWidth - w) / 2;
    }
  }

  // send to printer
  // ignore offsets
  // UINT offsetX = GetDeviceCaps(hPrtDC, PHYSICALOFFSETX);
  // UINT offsetY = GetDeviceCaps(hPrtDC, PHYSICALOFFSETY);
  SetStretchBltMode(hPrtDC, HALFTONE);
  StretchBlt(hPrtDC, pX, pY, w, h, hMemDC, mX, mY, mWidth, mHeight, SRCCOPY);

  // release
  DeleteObject(hBitmap);
  EndPage(hPrtDC);
  EndDoc(hPrtDC);
  DeleteDC(hMemDC);
  DeleteDC(hPrtDC);
  // GdiplusShutdown(gdiplusToken);

  return jobId;
}

Napi::Value _getJob(Napi::Env env, std::string printerName, int jobId) {
  // open printer
#ifdef UNICODE
  TCHAR pPrinterName[256];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    printerName.c_str(), printerName.size() + 1,
    pPrinterName, ARRAYSIZE(pPrinterName)
  );
#else
  LPTSTR pPrinterName = (LPTSTR)printerName.c_str();
#endif
  HANDLE hPrinter;
  BOOL fnReturn = OpenPrinter(pPrinterName, &hPrinter, NULL);
  if (!fnReturn){
    Napi::TypeError::New(env, "Open Printer failed")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  // GetJob
  DWORD dwNeeded = 0;
  JOB_INFO_1* pInfo = NULL;
  fnReturn = GetJob(hPrinter, jobId, 1L, (LPBYTE)NULL, 0L, &dwNeeded);
  if (dwNeeded > 0) {
    pInfo = (JOB_INFO_1*)GlobalAlloc(GPTR, dwNeeded);
    fnReturn = GetJob(hPrinter, jobId, 1L, (LPBYTE)pInfo, dwNeeded, &dwNeeded);
    if (!fnReturn) {
      report_error(L"GetJob failed.", GetLastError());
      GlobalFree(pInfo);
      ClosePrinter(hPrinter);
      return env.Null();
    }

    // fill object
    Napi::Object job = Napi::Object::New(env);
    job.Set("jobId", Napi::Number::New(env, pInfo->JobId));
    job.Set("document", Napi::String::New(env, (char16_t *)pInfo->pDocument));
    // if (pInfo->pStatus) {
    //   job.Set("statusText", Napi::String::New(env, pInfo->pStatus));
    // }
    job.Set("status", Napi::Number::New(env, pInfo->Status));
    // always determined by Status
    if (pInfo->Status & JOB_STATUS_PAUSED) {
      job.Set("statusText", Napi::String::New(env, "PSUSED"));
    } else if (pInfo->Status & JOB_STATUS_ERROR) {
      job.Set("statusText", Napi::String::New(env, "ERROR"));
    } else if (pInfo->Status & JOB_STATUS_DELETING) {
      job.Set("statusText", Napi::String::New(env, "DELETING"));
    } else if (pInfo->Status & JOB_STATUS_OFFLINE) {
      job.Set("statusText", Napi::String::New(env, "OFFLINE"));
    } else if (pInfo->Status & JOB_STATUS_PAPEROUT) {
      job.Set("statusText", Napi::String::New(env, "PAPEROUT"));
    } else if (pInfo->Status & JOB_STATUS_PRINTING) {
      job.Set("statusText", Napi::String::New(env, "PRINTING"));
    } else if (pInfo->Status & JOB_STATUS_PRINTED) {
      job.Set("statusText", Napi::String::New(env, "PRINTED"));
    } else if (pInfo->Status & JOB_STATUS_DELETED) {
      job.Set("statusText", Napi::String::New(env, "DELETED"));
    } else if (pInfo->Status & JOB_STATUS_RESTART) {
      job.Set("statusText", Napi::String::New(env, "RESTART"));
    } else if (pInfo->Status & JOB_STATUS_COMPLETE) {
      job.Set("statusText", Napi::String::New(env, "COMPLETE"));
    } else if (pInfo->Status & JOB_STATUS_RETAINED) {
      job.Set("statusText", Napi::String::New(env, "RETAINED"));
    }

    GlobalFree(pInfo);
    return job;
  }

  // close
  ClosePrinter(hPrinter);
  return env.Null();
}

BOOL  _setJob(Napi::Env env, std::string printerName, int jobId, DWORD control) {
  // open printer
#ifdef UNICODE
  TCHAR pPrinterName[256];
  MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,
    printerName.c_str(), printerName.size() + 1,
    pPrinterName, ARRAYSIZE(pPrinterName)
  );
#else
  LPTSTR pPrinterName = (LPTSTR)printerName.c_str();
#endif
  HANDLE hPrinter;
  BOOL fnReturn = OpenPrinter(pPrinterName, &hPrinter, NULL);
  if (!fnReturn){
    Napi::TypeError::New(env, "Open Printer failed")
        .ThrowAsJavaScriptException();
    return FALSE;
  }

  // TODO
  
  // close
  ClosePrinter(hPrinter);
  return FALSE;
}